mono和.net core在gc上的区别
简单的测试代码:
1 | class TestObject |
变化和对比:
- 在timer中引用this与否(使用
this.id
还是临时变量mid
) - timer是否结束
- timer结束后是否显式删除订阅(用即时定义delegate的方式显然就不能删除了,因此只能用成员函数的方式)
实验一
timer不结束,且引用了成员变量this.id
。
.netcore和mono都一样,无论创建多少个对象,gc时一个都回收不了。
原因:timer在运行,是活跃对象,且timer引用了this,导致this不能gc。
实验二
timer不结束,将this.id
改为临时变量mid
.netcore和mono都一样,所有对象都能gc
原因:timer虽然都是活跃的,但不再引用this,this被gc,在析构中停止timer,一起over。
实验三
timer过一会自己停止,还是引用this.id
。
这时就出现差别了:
.netcore:在timer停止后,所有对象都能立即被gc(立即意指调用一次GC.Collect
的结果)
mono:在timer停止后,大部分对象能被gc,但某些对象能活很久,只有不停创建新对象的过程中,那些老对象才看似随机的被gc掉,甚至出现3号对象一直不gc,4/5/6都gc了,创建7号时才看到3号gc的现象。而且很大概率总有1个对象长久不能gc,剩余对象数为0的情形很难出现一次。
实验四
在三的基础上,将匿名函数改为成员函数,在timer停止后再删除订阅
.netcore:与之前一样,都能gc。且无论删除订阅与否,都很快gc完。
mono:也全都能gc了😂。但是如果不删除订阅,则表现与三一样,有的对象活很久很难全部彻底gc。
结论
- mono gc看起来更保守,.netcore则更加“激进彻底”
- 从三、四对比来看,事件订阅是否删除,很大程度影响了mono gc的判定,理论上即使不删除,owner与timer都已经是不可达对象,但为什么删除后gc就很快?猜测可能是timer的内部实现,也许还有另一个可达列表记录(缓存)着所有timer,视其激活与否来延迟摘掉它,所以表现为最终状态都会gc,但有时候延迟很久,而删除定阅则从根本上解除了owner的所有引用,不再被timer这条灰线牵绊。
起因
之所以颇费心思测试对比这个,是因为在写xamarin程序时的一点遭遇:
- 发现有的Page在关闭后还会响应网络事件
- 当然最好的做法是为Page实现
OnDisappearing
函数,在这里注销事件 - 但是想着Page在关闭后应该没有引用了,会被gc, 那只要在添加事件订阅时使用
WeakReference
技巧,就可以在检测到gc时自动删除订阅,比2中的方法省事一点 - 结果却发现Page一直没有gc,最后不断删除无关代码排除大法,定位到是一个timer的使用所致。虽然当时就想到可能与订阅有关,并且改成成员函数式的订阅加删除后,确实就会gc了,但还是想剥离关键代码做一个专门的测试
- 初次测试由于没考虑到.netcore和mono的差别,只在.netcore环境下跑,结果发现完全没有问题,导致对本已解决的xamarin bug又自我怀疑,做了大量重复排查的无用功
- 终于想到xamarin是基于mono,而通常新建的console app都是基于.netcore,两者可能在内部实现上并不一样,于是再分别测试各种情况,最终确认mono版的表现与xamarin上是一致的
参考:
https://docs.microsoft.com/en-us/dotnet/standard/net-standard